/**   
* @Title: CustomizedRepositoryImpl.java
* @Package org.xaoyaoyao.cloud.jpa.repository.support
* @Description: TODO
* @author xaoyaoyao
* @date Feb 12, 2019 8:51:48 AM
* @version V1.0   
*/
package org.xaoyaoyao.cloud.jpa.repository.support;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

import javax.persistence.Column;
import javax.persistence.EntityManager;
import javax.persistence.Id;
import javax.persistence.Query;
import javax.persistence.Table;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.Session;
import org.hibernate.query.NativeQuery;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.xaoyaoyao.cloud.jpa.adapter.BeanTransformerAdapter;
import org.xaoyaoyao.cloud.jpa.repository.CustomizedRepository;
import org.xaoyaoyao.cloud.jpa.utils.BeanUtilsEx;
import org.xaoyaoyao.cloud.jpa.utils.ReflectHelper;
import org.xaoyaoyao.cloud.jpa.utils.UUIDUtils;

import lombok.extern.slf4j.Slf4j;

/**
 * @ClassName: CustomizedRepositoryImpl
 * @Description: TODO
 * @author xaoyaoyao
 * @date Feb 12, 2019 8:51:48 AM
 * 
 */
@Slf4j
public class CustomizedRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements CustomizedRepository<T, ID> {

	private final EntityManager entityManager;

	private Class<T> entityClass;

	public CustomizedRepositoryImpl(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
		super(entityInformation, entityManager);
		this.entityManager = entityManager;
	}

	public CustomizedRepositoryImpl(Class<T> domainClass, EntityManager entityManager) {
		super(domainClass, entityManager);
		this.entityManager = entityManager;
		this.entityClass = domainClass;
	}

	@Transactional
	@Override
	public List<T> saveBatch(List<T> entities) {
		if (CollectionUtils.isNotEmpty(entities)) {
			List<T> results = new ArrayList<T>(entities.size());
			entities.stream().forEach(entity -> {
				T t = save(entity);
				results.add(t);
			});
			return results;
		}
		return null;
	}

	@Transactional
	@Override
	public T update(T entity) {
		return saveAndFlush(entity);
	}

	@Transactional
	@Override
	public T updateSelective(T entity, ID id) {
		if (null != id) {
			Optional<T> optional = this.findById(id);
			T old = optional.get();
			BeanUtilsEx.copyProperties(entity, old);
			return update(old);
		}
		return null;
	}

	@Transactional
	@Override
	public void updateBatch(List<T> entities) {
		entities.stream().forEach(entity -> {
			update(entity);
		});
	}

	@SuppressWarnings("unchecked")
	@Override
	public List<Object[]> listBySQL(String sql) {
		return entityManager.createNativeQuery(sql).getResultList();
	}

	@Override
	public void store(Object... item) {
		if (null != item) {
			for (Object entity : item) {
				innerSave(entity);
			}
		}
	}

	@Override
	public void update(Object... item) {
		if (null != item) {
			for (Object entity : item) {
				entityManager.merge(entity);
			}
		}
	}

	@Override
	@Transactional
	public int updateByPrimaryKey(Object entity) {
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>>> entity >> {}", entity);
		}
		Assert.notNull(entity, "The entity is empty!");
		Class clazz = entity.getClass();
		String tableName = getTableName(clazz);
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>> Table name is empty!!!");
		}
		Assert.notNull(tableName, "The " + clazz.getName() + " table name is empty!");
		String primaryColumnKey = getPrimaryKey(clazz);
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>> Primary key is empty!!!");
		}
		Assert.notNull(primaryColumnKey, "The " + clazz.getName() + " primary key is empty!");
		// 属性值
		Map<String, Object> properties = getTableProperties(entity, clazz);
		Assert.notEmpty(properties, "The " + clazz.getName() + " attribute values are all empty!");
		Object primaryKeyVal = properties.get(primaryColumnKey);
		Assert.notNull(primaryKeyVal, "The primary key of the " + tableName + " table is empty!");
		return updateValue(properties, tableName, primaryColumnKey, primaryKeyVal);
	}

	@SuppressWarnings("rawtypes")
	private String getPrimaryKey(Class clazz) {
		Field[] fields = clazz.getDeclaredFields();
		for (Field field : fields) {
			if (field.isAnnotationPresent(Id.class)) {
				Column declaredAnnotation = field.getDeclaredAnnotation(Column.class);
				String column = declaredAnnotation.name();
				if (StringUtils.isNotBlank(column)) {
					return field.getName();
				}
			}
		}
		return null;
	}

	@Override
	@Transactional
	public int updateSelective(Object entity, Object primaryKeyVal) {
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>>> entity >> {}, primaryKeyVal >> {}", primaryKeyVal, entity);
		}
		Assert.notNull(primaryKeyVal, "The primaryKeyVal is empty!");
		Assert.notNull(entity, "The entity is empty!");
		Class clazz = entity.getClass();
		String tableName = getTableName(clazz);
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>> Table name is empty!!!");
		}
		Assert.notNull(tableName, "The " + clazz.getName() + " table name is empty!");
		String primaryColumnKey = getPrimaryKey(clazz);
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>> Primary key is empty!!!");
		}
		Assert.notNull(primaryColumnKey, "The " + clazz.getName() + " primary key is empty!");
		// 属性值
		Map<String, Object> properties = getTableProperties(entity, clazz);
		Assert.notEmpty(properties, "The " + clazz.getName() + " attribute values are all empty!");
		return updateValue(properties, tableName, primaryColumnKey, primaryKeyVal);
	}

	private int updateValue(Map<String, Object> properties, String tableName, String primaryColumnKey, Object primaryKeyVal) {
		// 拼接updateSQL语句
		int size = properties.size(), index = 0;
		List<Object> params = new ArrayList<>(size);
		StringBuffer nativeSql = new StringBuffer();
		nativeSql.append("UPDATE ").append(tableName).append(" SET ");
		for (Map.Entry<String, Object> entry : properties.entrySet()) {
			index++;
			String key = entry.getKey();
			Object val = entry.getValue();
			nativeSql.append(key).append(" = ").append("?");
			params.add(val);
			if (index < size) {
				nativeSql.append(", ");
			}
		}
		nativeSql.append(" WHERE ").append(primaryColumnKey).append(" = ?");
		params.add(primaryKeyVal);
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>>> sql >> {}, params >> {}", nativeSql.toString(), params);
		}
		// 执行update语句
		return this.executeUpdate(nativeSql.toString(), params.toArray());
	}

	@Override
	@Transactional
	public int update(Object setEntity, Object whereEntity) {
		Assert.notNull(setEntity, "The setEntity is empty!");
		Assert.notNull(whereEntity, "The whereEntity is empty!");
		Class clazz = setEntity.getClass();
		String tableName = getTableName(clazz), name = getTableName(whereEntity.getClass());
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>> Table name is empty!!!");
		}
		Assert.notNull(tableName, "The " + clazz.getName() + " table name is empty!");
		// 两个表名不一致则跳出
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>> setEntity tableName >> {}, whereEntity tableName >> {}, Inconsistent two table names!!!", tableName, name);
		}
		Assert.isTrue(tableName.equals(name), "Two different table names: setEntity table name: " + tableName + ", whereEntity table name: " + name);
		// 属性值
		Map<String, Object> setProperties = getTableProperties(setEntity, clazz);
		Assert.notEmpty(setProperties, "The " + clazz.getName() + "<setEntity> updated attribute values are all empty!");
		Map<String, Object> whereProperties = getTableProperties(whereEntity, clazz);
		Assert.notEmpty(whereProperties, "The " + clazz.getName() + "<whereEntity> update attribute values are all empty!");
		// 拼接updateSQL语句
		int setSize = setProperties.size(), index = 0, whereSize = whereProperties.size();
		int total = setSize + whereSize;
		List<Object> params = new ArrayList<>(total);
		StringBuffer nativeSql = new StringBuffer();
		nativeSql.append("UPDATE ").append(tableName).append(" SET ");
		// set部分
		for (Map.Entry<String, Object> entry : setProperties.entrySet()) {
			index++;
			String key = entry.getKey();
			Object val = entry.getValue();
			nativeSql.append(key).append(" = ").append("?");
			params.add(val);
			if (index < setSize) {
				nativeSql.append(", ");
			}
		}
		// where部分
		index = 0;
		nativeSql.append(" WHERE ");
		for (Map.Entry<String, Object> entry : whereProperties.entrySet()) {
			index++;
			String key = entry.getKey();
			Object val = entry.getValue();
			nativeSql.append(key).append(" = ").append("?");
			params.add(val);
			if (index < whereSize) {
				nativeSql.append(" AND ");
			}
		}
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>>> sql >> {}, params >> {}", nativeSql.toString(), params);
		}
		// 执行update语句
		return this.executeUpdate(nativeSql.toString(), params.toArray());
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private String getTableName(Class clazz) {
		Table table = (Table) clazz.getAnnotation(Table.class);
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>>> table >> {}", table);
		}
		if (null != table) {
			return table.name();
		}
		return null;
	}

	@SuppressWarnings("rawtypes")
	private Map<String, Object> getTableProperties(Object entity, Class clazz) {
		Map<String, Object> properties = new ConcurrentHashMap<>();
		Map<String, String> data = getColumnNames(clazz);
		// 获取Bean
		BeanWrapper srcBean = new BeanWrapperImpl(entity);
		// 获取Bean的属性描述
		PropertyDescriptor[] pds = srcBean.getPropertyDescriptors();
		for (PropertyDescriptor propertyDescriptor : pds) {
			String propertyName = propertyDescriptor.getName();
			Object propertyValue = srcBean.getPropertyValue(propertyName);
			if (log.isDebugEnabled()) {
				log.debug(">>>>>>>>> propertyName >> {}, propertyValue >> {}", propertyName, propertyValue);
			}
			// 只获取非空的属性
			if (null != propertyValue) {
				// 获取属性对应的字段
				String columnName = getColumnName(data, propertyName);
				if (StringUtils.isNotBlank(columnName)) {
					properties.put(columnName, propertyValue);
				}
			}
		}
		if (log.isDebugEnabled()) {
			log.debug(">>>>>>>>> properties >> {}", properties);
		}
		return properties;
	}

	@SuppressWarnings("rawtypes")
	private Map<String, String> getColumnNames(Class clazz) {
		Map<String, String> data = new ConcurrentHashMap<>();
		Field[] fields = clazz.getDeclaredFields();
		for (Field field : fields) {
			if (field.isAnnotationPresent(Column.class)) {
				// 获取字段名
				Column declaredAnnotation = field.getDeclaredAnnotation(Column.class);
				String column = declaredAnnotation.name();
				if (StringUtils.isNotBlank(column)) {
					data.put(field.getName(), column);
				}
			}
		}
		return data;
	}

	private String getColumnName(Map<String, String> data, String fieldName) {
		// 去掉类属性
		if (StringUtils.isBlank(fieldName) || fieldName.toUpperCase().equals("CLASS") || MapUtils.isEmpty(data)) {
			return null;
		}
		return data.get(fieldName);
	}

	@Override
	public int executeUpdate(String qlString, Object... values) {
		Query query = entityManager.createQuery(qlString);
		if (values != null) {
			for (int i = 0; i < values.length; i++) {
				query.setParameter(i + 1, values[i]);
			}
		}
		return query.executeUpdate();
	}

	@Override
	public int executeUpdate(String qlString, Map<String, Object> params) {
		Query query = entityManager.createQuery(qlString);
		for (String name : params.keySet()) {
			query.setParameter(name, params.get(name));
		}
		return query.executeUpdate();
	}

	@Override
	public int executeUpdate(String qlString, List<Object> values) {
		Query query = entityManager.createQuery(qlString);
		for (int i = 0; i < values.size(); i++) {
			query.setParameter(i + 1, values.get(i));
		}
		return query.executeUpdate();
	}

	/**
	 * 保存对象
	 * 
	 * @param item
	 *            保存对象
	 * @return
	 */
	private Serializable innerSave(Object item) {
		try {
			if (item == null)
				return null;
			Class<?> clazz = item.getClass();
			Field idField = ReflectHelper.getIdField(clazz);
			Method getMethod = null;
			if (idField != null) {
				Class<?> type = idField.getType();
				Object val = idField.get(item);
				if (type == String.class && (val == null || "".equals(val))) {
					idField.set(item, UUIDUtils.uuid());
				}
			} else {
				Method[] methods = clazz.getDeclaredMethods();
				for (Method method : methods) {
					Id id = method.getAnnotation(Id.class);
					if (id != null) {
						Object val = method.invoke(item);
						if (val == null || "".equals(val)) {
							String methodName = "s" + method.getName().substring(1);
							Method setMethod = clazz.getDeclaredMethod(methodName, method.getReturnType());
							if (setMethod != null) {
								setMethod.invoke(item, UUIDUtils.uuid());
							}
						}
						getMethod = method;
						break;
					}
				}
			}
			entityManager.persist(item);
			entityManager.flush();
			if (idField != null) {
				return (Serializable) idField.get(item);
			}
			if (getMethod != null) {
				return (Serializable) getMethod.invoke(item);
			}
			return null;
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings({ "unchecked", "deprecation", "rawtypes" })
	@Override
	public HashMap<String, Object> sqlQuery(String queryString, String countSql, Map<String, ?> values, int offset, int limit, String countName, String rowsName) {
		Assert.hasText(queryString, "queryString不能为空");
		Assert.hasText(countSql, "countSql不能为空");
		HashMap<String, Object> map = new HashMap<String, Object>();
		Query query = entityManager.createNativeQuery(queryString);
		Query countQuery = entityManager.createNativeQuery(countSql);
		// 给条件赋上值
		if (values != null && !values.isEmpty()) {
			for (Map.Entry<String, ?> entry : values.entrySet()) {
				query.setParameter(entry.getKey(), entry.getValue());
				countQuery.setParameter(entry.getKey(), entry.getValue());
			}
		}
		query.setFirstResult(offset);
		query.setMaxResults(limit);
		query.unwrap(NativeQuery.class).setResultTransformer(new BeanTransformerAdapter(this.entityClass));
		Object results = query.getResultList();
		Object resultsCount = countQuery.getSingleResult();
		map.put(countName, resultsCount);
		map.put(rowsName, results);
		return map;
	}

	@SuppressWarnings({ "rawtypes", "unchecked", "deprecation" })
	@Override
	public List<T> sqlQuery(String queryString, Map<String, ?> values) {
		Assert.hasText(queryString, "queryString不能为空");
		Session session = entityManager.unwrap(org.hibernate.Session.class);
		NativeQuery query = session.createSQLQuery(queryString);
		if (values != null) {
			query.setProperties(values);
		}
		query.setResultTransformer(new BeanTransformerAdapter(this.entityClass));
		return query.list();
	}

	@SuppressWarnings({ "unchecked", "deprecation", "rawtypes" })
	@Override
	public List<T> sqlQuery(String queryString, Object... values) {
		Assert.hasText(queryString, "queryString不能为空");
		Query query = entityManager.createNativeQuery(queryString);
		if (values != null) {
			for (int i = 0; i < values.length; i++) {
				query.setParameter(i + 1, values[i]);
			}
		}
		query.unwrap(NativeQuery.class).setResultTransformer(new BeanTransformerAdapter(this.entityClass));
		return query.getResultList();
	}

	@Override
	public HashMap<String, Object> retrieve(String queryString, String countSql, Map<String, ?> values, int offset, int limit, String countName, String rowsName) {
		Assert.hasText(queryString, "queryString不能为空");
		Assert.hasText(countSql, "countSql不能为空");
		HashMap<String, Object> map = new HashMap<String, Object>();
		Query query = entityManager.createQuery(queryString);
		Query countQuery = entityManager.createQuery(countSql);
		// 给条件赋上值
		if (values != null && !values.isEmpty()) {
			for (Map.Entry<String, ?> entry : values.entrySet()) {
				query.setParameter(entry.getKey(), entry.getValue());
				countQuery.setParameter(entry.getKey(), entry.getValue());
			}
		}
		query.setFirstResult(offset);
		query.setMaxResults(limit);
		Object results = query.getResultList();
		Object resultsCount = countQuery.getSingleResult();
		map.put(countName, resultsCount);
		map.put(rowsName, results);
		return map;
	}
}
